Skip to content

os/v3/puppy: a console-TUI dialog loses its question before its hint - #1028

Merged
bong-water-water-bong merged 6 commits into
mainfrom
os/v3-tui-modal
Nov 4, 2026
Merged

bong-water-water-bong merged 6 commits into
mainfrom
os/v3-tui-modal

Conversation

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator

os/v3/puppy: a console-TUI dialog loses its question before its hint

draw() sizes the modal box to its body and clamps it to the frame (bh = (body.len() + 2).min(area.height)), then
draws the body with Paragraph, so a body that does not fit is clipped at its END - and the last line of every dialog
is the hint, the only row that says how to answer.

Measured on the shipped static binary before this commit (main 434db3d2, 0edbe573…), real ptys, TERM=linux, key
1 on a blank-key box:

80x25, 60x12 y mints one and restarts Lemonade, n cancels intact
40x12 y mints one and restarts half
40x11, 40x10, 30x12, 24x12 absent

Modal::Confirm { yes: Yes::Reboot } carries y reboots the box, n cancels and Modal::Power carries
r reboots, p powers off, any other key cancels, so in a 40x11 ssh window a screen that is about to reboot the box
explains neither key that reboots it - while y still does what the question says. No gate impact (the gates run at
80x25 and 240x67, where the whole box fits).

The fix is the same rule the rest of this file follows - the block's most load-bearing row is the last one to go:
the dialog is now title | question | hint as three sections, and a frame too short for all of it loses the question
rows first (whole rows at a time), then the title, and never the hint.

40x12 after key 1: hint whole (y mints one and restarts / Lemonade, n cancels), one question row given up
40x11 : hint whole, two question rows given up
24x12 : hint whole and wrapped (y mints one and / restarts Lemonade, n / cancels)
80x25, 240x67 : unchanged - the whole box, byte-identical frames

the_modal_hint_is_never_the_row_that_goes renders four dialogs (reboot confirm, blank-key confirm, power, the API
key info box) at 24x12, 30x12, 40x10, 40x11, 40x12, 60x12, 80x25 and 240x67, and requires the frame to be the terminal
and the hint to be on screen, whole, at every one of them. Putting the old body construction back turns it red at
24x12; the fix turns it green.

Measured on the static binary c046551247f07569 (1,556,728 B, no PT_INTERP): cargo test 47/47, clippy
--all-targets -D warnings clean, cargo fmt --check clean, tools/copyright.py --check rc 0,
--snapshot 80x25 --fixture matches the committed frame. 2 files +111/−8, no new files, no gate file touched
(vm-check.sh, puppy-check.sh, menugate.sh untouched), no image build, no board run, no device ticket.

Evidence

  • Reproduced first on the shipped binary (main 434db3d2, 0edbe573…) in real ptys whose pane is exactly the size
    given: key 1 at 40x12 -> y mints one and restarts; 40x11 / 40x10 / 30x12 / 24x12 -> no hint line at all.
  • After this tree, same ptys: 40x12 and 40x11 -> the hint whole (y mints one and restarts / Lemonade, n cancels),
    24x12 -> the hint whole and wrapped over three rows.
  • the_modal_hint_is_never_the_row_that_goes (4 dialogs × 8 sizes) is red with the old body construction at 24x12 and
    green with the fix.
  • 80x25 and 240x67 are unchanged: the committed snapshot frames are byte-compared by renders_at_the_two_console_sizes.

Not in this PR

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

#1028 ready for review at 1c50d986 (rebased on main bdcd71e6; main has not touched os/v3/puppy/console-tui/ since the base, so this is conflict-free).

The one measurement to check: a pty whose pane is exactly 40x11, key 1. Before this branch that screen drew the key-confirm question with no hint line at all (y mints one and restarts Lemonade, n cancels), and y still minted; the same size now keeps the hint whole and gives up question rows instead. 80x25 and 240x67 are byte-identical to the committed snapshots.

Independent of #1026 (f245bb9e): different hunks, different block, either merge order is safe - both touch the README's test-count line with the same new value, so nothing there to resolve.

`draw()` sizes the modal box to its body and clamps it to the frame (`bh = (body.len() + 2).min(area.height)`), then
draws the body with `Paragraph`, so a body that does not fit is clipped at its END - and the last line of every dialog
is the hint, the only row that says how to answer.

Measured on the shipped static binary before this commit (main `434db3d2`, `0edbe573…`), real ptys, `TERM=linux`, key
**1** on a blank-key box:

  80x25, 60x12   `y mints one and restarts Lemonade, n cancels`  intact
  40x12          `y mints one and restarts`                      half
  40x11, 40x10, 30x12, 24x12                                     absent

`Modal::Confirm { yes: Yes::Reboot }` carries `y reboots the box, n cancels` and `Modal::Power` carries
`r reboots, p powers off, any other key cancels`, so in a 40x11 ssh window a screen that is about to reboot the box
explains neither key that reboots it - while `y` still does what the question says. No gate impact (the gates run at
80x25 and 240x67, where the whole box fits).

The fix is the same rule the rest of this file follows - the block's most load-bearing row is the last one to go:
the dialog is now `title | question | hint` as three sections, and a frame too short for all of it loses the question
rows first (whole rows at a time), then the title, and never the hint.

  40x12 after key 1: hint whole (`y mints one and restarts` / `Lemonade, n cancels`), one question row given up
  40x11            : hint whole, two question rows given up
  24x12            : hint whole and wrapped (`y mints one and` / `restarts Lemonade, n` / `cancels`)
  80x25, 240x67    : unchanged - the whole box, byte-identical frames

`the_modal_hint_is_never_the_row_that_goes` renders four dialogs (reboot confirm, blank-key confirm, power, the API
key info box) at 24x12, 30x12, 40x10, 40x11, 40x12, 60x12, 80x25 and 240x67, and requires the frame to be the terminal
and the hint to be on screen, whole, at every one of them. Putting the old body construction back turns it red at
24x12; the fix turns it green.

Measured on the static binary `42e11547b870428c` (1,556,736 B, no `PT_INTERP`): `cargo test` 47/47, clippy
`--all-targets -D warnings` clean, `cargo fmt --check` clean, `tools/copyright.py --check` rc 0,
`--snapshot 80x25 --fixture` matches the committed frame. 2 files +~85/-17, no new files, no gate file touched, no
image build, no board run, no device ticket.

Closes #1027

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Head moved to 7ba4e7f3 for one reason: a comment in the diff said the reboot dialog was Reboot the box now? and the dialog's own words are Reboot the appliance now?. Content otherwise identical (git diff 1c50d986 7ba4e7f3 is that comment, 2 lines in / 2 lines out).

Artifact hash correction, so nobody chases a ghost: the tree of this head builds 42e11547b870428c, 1,556,736 B - not the c046551247f07569 (1,556,728 B) the earlier comments quote, which is the pre-comment-fix tree. Measured, not assumed: both trees were built twice and each pair is byte-identical, so a comment-only change really does move this binary by 8 bytes (this repo already knows rustc embeds source locations - #1014's state file has the same note for 274811c2… vs 0edbe573…). Behaviourally identical; 42e11547… is what a rebuild from this head produces.

CI is re-running on the new head.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at 463ad8792

$ cd os/v3/puppy/console-tui && cargo test --offline
test result: ok. 47 passed; 0 failed; 0 ignored

The added test is the_modal_hint_is_never_the_row_that_goes, and it pins the property the diff implements: for four real dialogs (reboot confirm, blank-key confirm, power menu, info box) at 24x12 / 30x12 / 40x10 / 40x11 / 40x12 / 60x12 …, the hint row is always drawn, so y reboots the box, n cancels can no longer be the thing that falls off a frame where y still reboots.

The code change is the right shape for it: title_rows / text_rows / hint_rows are wrapped separately, and the trimming loop takes the explanation first (whole rows), then the title, never the hint; the box height is recomputed from what survived, so bh can no longer exceed the frame and ratatui cannot clip the end.

Note for the merge order, not a defect: this and #1026 both rewrite regions of os/v3/puppy/console-tui/src/ui.rs and its README (this one the modal block at ~851, #1026 the footer at ~704 plus the draw() prologue). They are independent hunks and both are green, but whichever lands second needs its rebase re-run — the §"frame is the terminal" tests are the ones that would catch a bad merge.

@bong-water-water-bong bong-water-water-bong added the tprs-ready TPRS: review verdict READY posted by the standing reviewer; merge gate satisfied label Nov 4, 2026

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at 7ba4e7f33

Rebase only, checked as content rather than as counters: the +/- lines of the PR's own diff are identical at 463ad8792 (the head I reviewed) and at 7ba4e7f (the diff against the moving merge base showed +111 and now +109 only because main's context moved). cargo test --offline was 47 passed / 0 failed at 463ad8792, and ui.rs is unchanged since, so that result carries.

Same merge-order note as before: this and #1026 both rewrite console-tui/src/ui.rs (modal block here, footer + draw() prologue there); whichever lands second needs its rebase re-run, and the frame-is-the-terminal tests are what would catch a bad merge.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Updated to main (8c1eaea3): head 12907048, git diff 7ba4e7f3 HEAD -- os/v3/puppy/console-tui is empty, cargo test 47/47 on the merged tree. The reviewed diff has not moved; the artifact line in the commit message still holds (42e11547b870428c is this tree).

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at 12907048

Rebase-only, verified twice over:

own contribution (+/- lines vs merge base), 7ba4e7f33 vs 12907048:
  +109/-8, sha256 of the diff lines = 1e496b48eadb  (both heads)

$ cd os/v3/puppy/console-tui && cargo test --offline      (at 12907048, on main 8c1eaea3)
test result: ok. 47 passed; 0 failed; 0 ignored

12907048 is a merge of main (Merge branch 'main' into os/v3-tui-modal), so the diff against the merge base is identical to the head I reviewed; I re-ran the suite at the new head anyway since this and #1026 both rewrite ui.rs. 47/47, including the_modal_hint_is_never_the_row_that_goes (4 dialogs × 6 sizes), which is the property that closes #1027.

Review stands as posted. The merge-order note is the only thing left: this and #1026 are independent hunks of the same file, so whichever lands second should re-run cargo test after its merge — the frame-is-the-terminal assertions are what would catch a bad merge, and they are cheap.

…e order is conflict-free

No behaviour change, no line of logic moved: the modal test now sits above
`the_menu_rule_gives_way_before_an_action_does` (which #1026 appends after) and the README paragraph now sits above the
MENU-rule paragraph, so the two PRs no longer share merge context. Measured: before this, merging #1026 into this
branch conflicted on both os/v3/puppy/console-tui/{README.md,src/ui.rs}; after it, both orders merge cleanly and
`cargo test` is 48/48 on the combined tree.
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Head deb9a061 - placement only, no behaviour change, and it exists because of the note in the review: "whichever lands second should re-run cargo test after its merge".

Measured first: test-merging #1026 (39da6108) into this branch conflicted on both files (README.md, src/ui.rs) - the two PRs appended their tests at the same end of the module and inserted their README paragraph after the same anchor. So the modal test now sits above the_menu_rule_gives_way_before_an_action_does and the README paragraph above the MENU-rule paragraph. After that, both merge orders merge cleanly and the combined tree is 48/48:

#1026 -> #1028   Automatic merge went well; stopped before committing   48 passed
#1028 -> #1026   Automatic merge went well; stopped before committing   48 passed

One bookkeeping nit, recorded rather than hidden: both PRs set the README's test count to 47, so after both land the line says 47 while the suite is 48. Whichever merges second can bump it, or tell me and I will - it is a one-word README change and I did not want to put a third push on a PR that is already READY for a count.

The reviewed diff of the code is unchanged: git diff 12907048 deb9a061 -- os/v3/puppy/console-tui/src/ui.rs is the test moving, nothing else; cargo test 47/47 on this branch alone.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at deb9a06

Merge-only rebase, and this push also settles the merge-order question I raised, so I checked it the way it should be checked — by merging #1026 into this branch and running the suite:

own contribution (+/- lines vs merge base), 12907048 vs deb9a06: +109/-8, hash 1e496b48eadb (identical)

git merge origin/main            (main = 9315118, i.e. #1026 already in)
  -> clean, no conflicts ("placement only: keep this PR's hunks clear of #1026's, so either merge order is conflict-free")
cargo test --offline
  -> test result: ok. 48 passed; 0 failed; 0 ignored

48 = 46 base + a_message_gives_way_before_an_action_does (#1026, now on main) + the_modal_hint_is_never_the_row_that_goes (this PR). So both console-TUI PRs' tests run together against the merged tree and both pass — either merge order is safe, and the "whichever lands second must re-run" note is discharged.

Review stands as posted.

bong-water-water-bong added 2 commits November 4, 2026 18:00
#1026 merged as 9315118 and this branch is rebased onto it, so the count line is 46 + one test each = 48. It is the
one bookkeeping nit the #1028 review comment raised.
@bong-water-water-bong

bong-water-water-bong commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator Author

#1026 merged as 9315118d (17:53 AST), so this branch has been merged onto it: head 38b8e1d8, cargo test 48/48 (46 + one test from each PR), and the README count now says 48 - the one nit from the review is closed.

Two consequences worth stating because my earlier comments quote the old numbers:

Everything else stands: the reviewed modal diff is unchanged (git diff deb9a061 38b8e1d8 -- os/v3/puppy/console-tui/src/ui.rs is empty - the merge and the README count are the only changes), 80x25/240x67 frames byte-identical, every gate string unchanged.

(A previous version of this comment was mangled by shell backticks in transit; this one is the correction.)

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Re-verdict requested at 38b8e1d8 - the head has moved twice since the READY at 12907048: deb9a061 (placement-only: the modal test moved above the MENU-rule test and the README paragraph above the MENU-rule paragraph, so #1026 and this PR stop sharing merge context) and 38b8e1d8 (main merged, i.e. #1026's own change, plus the README count 47 -> 48).

For a fast re-check, the two things that could have moved: git diff 12907048 38b8e1d8 -- os/v3/puppy/console-tui/src/ui.rs is a test moving plus #1026's footer work arriving through the merge (no line of the modal fix changed), and cargo test --offline is 48/48 on this head. 80x25/240x67 frames byte-identical; gate strings unchanged.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at 38b8e1d8

Both pushes checked with the instrument that can actually see a reordering: the PR's own +/- lines.

deb9a06 (placement only)   vs 12907048 (the head I reviewed):
  own-diff hash 1e496b48eadb, identical -- +109/-8 both
  git diff 12907048 deb9a06: 98 lines moved in ui.rs + the README paragraph, i.e. main's MENU-rule test and
  paragraph relocated around this PR's hunk. Because the moved text exists identically at both ends, a move
  cancels out of the diff, which is exactly why the hash did not move: nothing this PR adds changed.

38b8e1d8 (merge main = #1026 aboard) vs 12907048:
  the ONLY difference in the PR's own lines is the README suite count, 47 -> 48:
    -cargo test   # 47 tests: ...
    +cargo test   # 48 tests: ...
  so the modal branch and `the_modal_hint_is_never_the_row_that_goes` are byte-identical to what I reviewed.

$ cd os/v3/puppy/console-tui && cargo test --offline     (at 38b8e1d8, main 9315118 = #1026 merged)
test result: ok. 48 passed; 0 failed; 0 ignored

48 = 46 base + #1026's a_message_gives_way_before_an_action_does + this PR's the_modal_hint_is_never_the_row_that_goes, and the count in the README now matches what the suite actually prints. I had already merge-tested this pair at deb9a06 (clean merge, 48/48); with #1026 now in main, this head is that same tree and the same result, so the merge-order question is settled in both directions.

Review stands as posted — the hint is never the row that goes, the trimming loop takes the explanation first and then the title, and bh is recomputed from what survived so ratatui cannot clip the end.

Not re-derived: the "80x25/240x67 frames byte-identical" claim. The committed test asserts the frame-is-the-terminal invariant and the gate strings at those sizes and passes, which covers the property from inside the suite; a frame-by-frame comparison against the base would be the only way to check byte-identity and the sizes you sweep are already covered by the 4 dialogs × 6 sizes assertions.

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

1026 - Partially compliant

Compliant requirements:

  • The footer construction now prioritizes showing the full message over cutting off actions
  • The legend's tail words are given way first, then the legend line itself
  • The nine actions are never charged as fixed rows
  • The message is never cut mid-word
  • Tests cover all specified frame sizes and verify correct behavior
  • The 80x25 and 240x67 frame sizes remain unchanged

Non-compliant requirements:

  • None

Requires further human verification:

  • None

1014 - Partially compliant

Compliant requirements:

  • The MENU rule now gives way before an action does
  • The rule is only drawn when it fits in addition to actions and footer
  • The layout no longer clips the end of the menu
  • Tests cover all specified frame sizes and verify correct behavior
  • The 80x25 and 240x67 frame sizes remain unchanged

Non-compliant requirements:

  • None

Requires further human verification:

  • None
⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Modal hint clipping

The fix correctly ensures that the hint (which contains the key instructions for answering the dialog) is never clipped, even in small terminal sizes. This prevents scenarios where a user might not know how to respond to a dialog because the hint is cut off, while the question is still visible. The implementation properly prioritizes keeping the hint by removing question rows first, then title rows, and never removes the hint.

let (title, text, hint) = modal_box(m);
let lines: Vec<String> = std::iter::once(title.clone())
    .chain(text.iter().cloned())
    .chain(std::iter::once(hint.clone()))
    .collect();
// the box is the longest line plus its own padding: sizing it to the longest line and then wrapping
// inside it made every hint wrap for no reason (measured: "any other key / cancels")
let width = (lines.iter().map(|l| l.chars().count()).max().unwrap_or(10) + 4)
    .min(w.saturating_sub(4))
    .max(27);
let wrap = |s: &str| -> Vec<Line<'static>> {
    wrap_words(s, width.saturating_sub(4))
        .into_iter()
        .map(|l| styled(l, Some(Color::White), false))
        .collect()
};
let mut title_rows = wrap(&title);
let mut text_rows: Vec<Line> = text.iter().flat_map(|l| wrap(l)).collect();
let hint_rows = wrap(&hint);
// The hint is the only row that says how to answer the question, so it is never the row that goes: a frame
// too short for the whole box loses the explanation first, whole rows at a time, and then the title. Before
// this the box was clamped to the frame and ratatui clipped the END - at 40x11 the reboot confirm drew
// `Reboot the appliance now?` with `y reboots the box, n cancels` off the screen while `y` still rebooted.
let room = (area.height as usize).saturating_sub(2);
while title_rows.len() + text_rows.len() + hint_rows.len() > room && !text_rows.is_empty() {
    text_rows.pop();
}
while title_rows.len() + hint_rows.len() > room && !title_rows.is_empty() {
    title_rows.pop();
}
let body: Vec<Line> = title_rows
    .into_iter()
    .chain(text_rows)
    .chain(hint_rows)
    .collect();
let bh = (body.len() as u16 + 2).min(area.height);
Footer layout logic

The new footer layout logic correctly prioritizes showing the full message over cutting off actions. It ensures that words give way in a fixed order: the legend's tail words first, then the legend line itself, and only if necessary does the message go. This prevents the scenario where the ninth action is clipped due to insufficient space.

if let Some(m) = &a.modal {

@bong-water-water-bong
bong-water-water-bong merged commit dc6cd32 into main Nov 4, 2026
9 checks passed
@bong-water-water-bong
bong-water-water-bong deleted the os/v3-tui-modal branch November 4, 2026 22:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Review effort 3/5 tprs-ready TPRS: review verdict READY posted by the standing reviewer; merge gate satisfied

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant